# -*- coding: utf-8 -*-
"""
xfree.plugins.models
~~~~~~~~~~~~~~~~~~~~~~~
这个模块提供了插件的注册功能以及基于数据库的基本键值存储，用于插件系统。


"""

from flask import current_app
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm.collections import attribute_mapped_collection

from xfree.extensions import db
from xfree.utils.database import CRUDMixin
from xfree.utils.forms import SettingValueType, generate_settings_form


class PluginStore(CRUDMixin, db.Model):
    """
    插件存储类，用于存储单个插件的配置键值对。

    属性：
    - id：整数类型的主键。
    - key：Unicode 字符串，表示配置项的键，不能为空。
    - value：使用 PickleType 存储配置项的值，可以存储各种 Python 对象。
    - value_type：枚举类型，来自 SettingValueType，表示值的类型，如字符串、整数、布尔值等。
    - extra：使用 PickleType 存储额外信息，如验证参数（最小长度、最大长度等）或对于选择字段的选项。
    - plugin_id：整数类型，外键关联到 PluginRegistry 表的 id，表示该配置项所属的插件。
    - name：Unicode 字符串，表示配置项的显示名称，不能为空。
    - description：文本类型，用于存储配置项的描述信息。

    约束：
    - __table_args__：定义了一个唯一约束，确保在同一个插件中，键是唯一的。

    方法：
    - __repr__：用于在调试时提供插件设置的可读表示形式。
    - get_or_create：类方法，根据插件 ID 和键查找插件存储项，如果不存在则创建一个空的存储项。
    """
    id = db.Column(db.Integer, primary_key=True)
    key = db.Column(db.Unicode(255), nullable=False)
    value = db.Column(db.PickleType, nullable=False)
    # 可用的类型：字符串、整数、浮点数、布尔值、单选、多选
    value_type = db.Column(db.Enum(SettingValueType), nullable=False)
    # 额外属性，如验证相关的内容（最小长度、最大长度等）
    # 对于 Select*Fields，需要提供：选项
    extra = db.Column(db.PickleType, nullable=True)
    plugin_id = db.Column(
        db.Integer, db.ForeignKey("plugin_registry.id", ondelete="CASCADE")
    )

    # 显示相关内容
    name = db.Column(db.Unicode(255), nullable=False)
    description = db.Column(db.Text, nullable=True)

    __table_args__ = (UniqueConstraint("key", "plugin_id", name="plugin_kv_uniq"),)

    def __repr__(self):
        """
        在调试时提供插件设置的可读表示形式。
        """
        return "<PluginSetting plugin={} key={} value={}>".format(
            self.plugin.name, self.key, self.value
        )

    @classmethod
    def get_or_create(cls, plugin_id, key):
        """
        返回 PluginStore 对象或者一个空对象。
        创建的对象仍然需要添加到数据库会话中。

        参数：
        - plugin_id：插件的 ID。
        - key：配置项的键。
        """
        obj = cls.query.filter_by(plugin_id=plugin_id, key=key).first()

        if obj is not None:
            return obj
        return PluginStore()


class PluginRegistry(CRUDMixin, db.Model):
    """
    插件注册类，表示插件的注册信息。

    属性：
    - id：整数类型的主键。
    - name：Unicode 字符串，唯一且不能为空，表示插件的名称。
    - enabled：布尔类型，默认值为 True，表示插件是否启用。
    - values：与 PluginStore 建立关系，通过 attribute_mapped_collection("key") 将插件存储项按照键进行映射，以便快速访问特定键的存储项。同时，设置了级联删除，确保当插件被删除时，相关的存储项也被删除。

    属性方法：
    - settings：返回一个字典，包含插件的所有设置，键为存储项的键，值为存储项的值。
    - info：返回插件的元数据信息，从当前应用的插件管理器中获取。
    - is_installable：检查插件是否有可安装的设置，通过检查插件模块是否有 SETTINGS 属性来确定。
    - is_installed：检查插件是否已安装，通过检查插件是否有设置项来确定。
    - get_settings_form：生成基于插件设置的表单对象，调用 generate_settings_form 函数并传入插件的存储项集合。
    - update_settings：更新插件的设置，接受一个字典，其中键是设置项的键，值是新的设置值。
    - add_settings：添加插件的设置，接受一个字典，其中键是设置项的键，值是包含设置项详细信息的字典。可以选择强制覆盖现有设置。

    方法：
    - __repr__：用于在调试时提供插件注册信息的可读表示形式。
    """
    # 模型有一个主键列（id），一个唯一非空列（name），一个布尔列（enabled），以及一个关系（values），该关系通过 attribute_mapped_collection("key") 将 PluginStore 对象按照键进行映射，以便快速访问特定键的存储项。同时，设置了级联删除，确保当插件被删除时，相关的存储项也被删除。
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode(255), unique=True, nullable=False)
    enabled = db.Column(db.Boolean, default=True)
    values = db.relationship(
        "PluginStore",
        collection_class=attribute_mapped_collection("key"),
        backref="plugin",
        cascade="all, delete-orphan",
    )

    @property
    def settings(self):
        """
        返回一个字典，包含插件的所有设置。
        键为存储项的键，值为存储项的值。
        """
        return {kv.key: kv.value for kv in self.values.values()}

    @property
    def info(self):
        """
        返回插件的元数据信息。
        从当前应用的插件管理器中获取。
        """
        return current_app.pluggy.list_plugin_metadata().get(self.name, {})

    @property
    def is_installable(self):
        """
        返回 True 如果插件有可以安装的设置。
        通过检查插件模块是否有 SETTINGS 属性来确定。
        """
        plugin_module = current_app.pluggy.get_plugin(self.name)
        return True if plugin_module.SETTINGS else False

    @property
    def is_installed(self):
        """
        返回 True 如果插件已安装。
        通过检查插件是否有设置项来确定。
        """
        if self.settings:
            return True
        return False

    def get_settings_form(self):
        """
        生成基于插件设置的表单对象。
        调用 generate_settings_form 函数并传入插件的存储项集合。
        """
        return generate_settings_form(self.values.values())()

    def update_settings(self, settings):
        """
        更新插件的设置。

        参数：
        - settings：一个字典，包含要更新的设置项。键是设置项的键，值是新的设置值。
        """
        pluginstore = PluginStore.query.filter(
            PluginStore.plugin_id == self.id, PluginStore.key.in_(settings.keys())
        ).all()

        setting_list = []
        for pluginsetting in pluginstore:
            pluginsetting.value = settings[pluginsetting.key]
            setting_list.append(pluginsetting)
        db.session.add_all(setting_list)
        db.session.commit()

    def add_settings(self, settings, force=False):
        """
        添加插件的设置。

        参数：
        - settings：一个字典，包含要添加的设置项。键是设置项的键，值是包含设置项详细信息的字典。
        - force：如果为 True，则强制覆盖现有设置。
        """
        plugin_settings = []
        for key in settings:
            if force:
                with db.session.no_autoflush:
                    pluginstore = PluginStore.get_or_create(self.id, key)
            else:
                # 否则我们假设不存在这样的设置项
                pluginstore = PluginStore()

            pluginstore.key = key
            pluginstore.plugin = self
            pluginstore.value = settings[key]["value"]
            pluginstore.value_type = settings[key]["value_type"]
            pluginstore.extra = settings[key]["extra"]
            pluginstore.name = settings[key]["name"]
            pluginstore.description = settings[key]["description"]
            plugin_settings.append(pluginstore)

        db.session.add_all(plugin_settings)
        db.session.commit()

    def __repr__(self):
        """
        在调试时提供插件注册信息的可读表示形式。
        """
        return "<Plugin name={} enabled={}>".format(self.name, self.enabled)

